﻿using LightCAD.MathLib;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace LightCAD.Three
{
    public class LoadActions
    {
        public Action<object> onLoad;
        public onProcessDelegate onProgress;
        public onErrorDelegate onError;
    }

    public class HttpError : Error
    {
        public object response;
        public HttpError(string message, object response) : base(message)
        {
            this.response = response;
        }
    }

    public class FileLoader : Loader<object>
    {
        public readonly static JsObj<string, ListEx<LoadActions>> loading = new JsObj<string, ListEx<LoadActions>>();
        public const int ByteLimit = 1024 * 512;

        private string responseType;
        private string mimeType;
        public FileLoader(LoadingManager manager) : base(manager)
        {
        }

        public override object load(string url, Action<object> onLoad, onProcessDelegate onProgress, onErrorDelegate onError = null)
        {
            url = getTrueUrl(url);
            var data = getCache(url, onLoad);
            if (data != null)
                return data;
            if (loading[url] != null)
            {
                //如果已经有其他线程访问该资源，等待其他线程跑完结果然后返回
                ManualResetEvent mre = new ManualResetEvent(true);
                mre.Reset();
                var loadAction = new LoadActions
                {
                    onLoad = (dt) => { onLoad(dt); mre.Set(); },
                    onProgress = onProgress,
                    onError = (err) => { onError(err); mre.Set(); }
                };
                loading[url].Push(loadAction);
                mre.WaitOne();
                return getCache(url, null);
            }
            initLoading(url, onLoad, onProgress, onError);
            var callbacks = loading[url];
            this.manager.itemStart(url);
            return loadProcess(url, callbacks);
        }

        public override async Task loadAsync(string url, Action<object> onLoad, onProcessDelegate onProgress, onErrorDelegate onError)
        {
            url = getTrueUrl(url);
            var data = getCache(url, onLoad);
            if (data != null)
                return;
            if (loading[url] != null)
            {
                var loadAction = new LoadActions
                {
                    onLoad = onLoad,
                    onProgress = onProgress,
                    onError = onError
                };
                loading[url].Push(loadAction);
                return;
            }
            initLoading(url, onLoad, onProgress, onError);
            var callbacks = loading[url];
            this.manager.itemStart(url);
            await Task.Run(() =>
            {
                loadProcess(url, callbacks);
            });
        }

        private string getTrueUrl(string url)
        {
            if (url == null) url = "";

            if (this.path != null) url = this.path + url;

            url = this.manager.resolveURL(url);

            if (!url.ToLower().StartsWith("http"))
            {
                if (url.StartsWith(".") || !url.Contains(":"))
                    url = AppDomain.CurrentDomain.BaseDirectory + url;
                url = @"file://" + url;
            }
            return url;
        }

        public object getCache(string url, Action<object> onLoad)
        {
            var cached = Cache.get(url);
            if (cached != null)
            {
                this.manager.itemStart(url);
                //setTimeout(() => {
                if (onLoad != null) onLoad(cached);
                this.manager.itemEnd(url);
                //}, 0);
                return cached;
            }
            return null;
        }

        private void initLoading(string url, Action<object> onLoad, onProcessDelegate onProgress, onErrorDelegate onError)
        {
            // Initialise array for duplicate requests
            loading[url] = new ListEx<LoadActions>();
            loading[url].Push(new LoadActions
            {
                onLoad = onLoad,
                onProgress = onProgress,
                onError = onError,
            });
        }

        public object loadProcess(string url, ListEx<LoadActions> callbacks)
        {
            object data = null;
            try
            {
                var response = returnResponse(url);
                var total = response.ContentLength;
                var byteList = new List<byte>();
                using (var fs = response.GetResponseStream())
                {
                    var buffer = new byte[ByteLimit];
                    refreshBuffer();
                    var readCount = 0;
                    do
                    {
                        readCount = fs.Read(buffer, 0, ByteLimit);
                        if (readCount == ByteLimit)
                        {
                            byteList.AddRange(buffer);
                            refreshBuffer();
                        }
                        else
                            for (int i = 0; i < readCount; i++)
                                byteList.Add(buffer[i]);
                        for (int i = 0; i < callbacks.Length; i++)
                        {
                            callbacks[i]?.onProgress?.Invoke(new ProgressEvent { target = url, loaded = byteList.Count, total = (int)total });
                        }

                    }
                    while (readCount > 0);
                    void refreshBuffer()
                    {
                        for (int i = 0; i < ByteLimit; i++)
                            buffer[i] = 0;
                    }
                    fs.Close();
                }
                data = returnResponseResult(byteList);
                Cache.add(url, data);
                loading.remove(url);
                for (int i = 0, il = callbacks.Length; i < il; i++)
                {

                    var callback = callbacks[i];
                    if (callback.onLoad != null) callback.onLoad(data);

                }
            }
            catch (Exception err)
            {
                callbacks = loading[url];
                loading.remove(url);
                if (callbacks == null)
                {
                    // When onLoad was called and url was deleted in `loading`
                    this.manager.itemError(url);
                    throw err;
                }
                for (int i = 0, il = callbacks.Length; i < il; i++)
                {
                    var callback = callbacks[i];
                    if (callback.onError != null) callback.onError(new ErrorEvent { target = url, exception = err });
                }
                this.manager.itemError(url);
            }
            finally
            {
                this.manager.itemEnd(url);
            }
            return data;
        }
        private WebResponse returnResponse(string url)
        {
            var myUrl = new Uri(url);
            WebRequest request = null;
            if (url.StartsWith("file:"))
                request = (FileWebRequest)WebRequest.CreateDefault(myUrl);
            else
                request = (HttpWebRequest)WebRequest.CreateDefault(myUrl);
            var response = request.GetResponse();
            return response;
        }
        private object returnResponseResult(List<byte> response)
        {
            switch (this.responseType)
            {
                case "arraybuffer":

                    return response.arrayBuffer();

                case "blob":

                //return response.blob();

                case "document":

                    //return response.text()
                    //    .then(text => {

                    //        const parser = new DOMParser();
                    //        return parser.parseFromString(text, mimeType);

                    //    });
                    return response.text();
                case "json":

                //return response.json();

                default:

                    if (string.IsNullOrEmpty(mimeType))
                    {

                        return response.text();

                    }
                    else
                    {

                        // sniff encoding
                        var re = new Regex("charset = \"?([^;\"\\s]*)\"?", RegexOptions.IgnoreCase);

                        var exec = re.Match(mimeType);
                        var label = exec != null && exec.Groups.Count > 0 && !string.IsNullOrEmpty(exec.Groups[1].Value) ? exec.Groups[1].Value.toLowerCase() : null;
                        Encoding decoder;
                        switch (label)
                        {
                            case "utf-8":
                            case "utf8":
                            default:
                                decoder = Encoding.UTF8;
                                break;
                        }
                        return decoder.GetString(response.arrayBuffer());

                    }
            }
        }
        public FileLoader setResponseType(string value)
        {
            this.responseType = value;
            return this;
        }

        public FileLoader setMimeType(string value)
        {

            this.mimeType = value;
            return this;

        }
    }
    public class ProgressEvent : EventArgs
    {
        public bool lengthComputable;
        public int loaded;
        public int total;
    }
    public class ErrorEvent : EventArgs
    {
        public Exception exception;
    }
    public static class ByteArrayExt
    {
        public static string text(this IList<byte> bytes)
        {
            return Encoding.UTF8.GetString(bytes.ToArray());
        }
        public static byte[] arrayBuffer(this IList<byte> bytes)
        {
            return bytes.ToArray();
        }
        //public static JObject json(this IList<byte> bytes)
        //{
        //    return JObject.Parse(bytes.text());
        //}
    }
}
